#include "gcpch.h"
#include "virtdir.h"
#include "virtdir_filesystem.h"
#include "msgmask.h"
#include "diritem.h"
#include "attributes.h"
#include "virtdir_processor.h"
#include "diritem_pool.h"

#include <windows.h>
#include <process.h>

enum geek_virtdir_filesystem_msg
{
    geek_vdfsm_quit = 1 << 0,
    geek_vdfsm_cancel_dirwalk = 1 << 1,
    geek_vdfsm_begin_dirwalk = 1 << 2
};

struct geek_virtdir_filesystem : geek_virtdir
{
    uintptr_t thread;
    geek_msgmask msgmask;

    volatile wchar_t *path;

    // the following vars are used in secondary thread only
    bool busy;
    HANDLE find_handle;
};

GEEK_STATIC_ASSERT(
    sizeof(geek_virtdir_filesystem) <= sizeof(geek_virtdir_blob),
    geek_virtdir_filesystem_should_be_smaller_or_equal_to_geek_virtdir_blob
    );

static void virtdir_filesystem_fill_diritem(geek_diritem *item, const WIN32_FIND_DATA *find_data)
{
    memset(item,0,sizeof(geek_diritem));

    wcscpy_s(item->display_name,find_data->cFileName);
    item->modify_time = find_data->ftLastWriteTime;

    if(find_data->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
    {
        item->attributes |= geek_attr_directory;
    }
}

static void virtdir_filesystem_begin_dirwalk(geek_virtdir_filesystem *virtdir)
{
    wchar_t *path = (wchar_t *)InterlockedExchangePointer(&virtdir->path,NULL);
    if(path != NULL)
    {
        WIN32_FIND_DATA find_data;
        virtdir->find_handle = FindFirstFile(path,&find_data);
        if(virtdir->find_handle != INVALID_HANDLE_VALUE)
        {
            geek_diritem *item = geek_diritem_pool_alloc();
            virtdir_filesystem_fill_diritem(item,&find_data);
            geek_virtdir_processor_on_new_diritem(virtdir->processor,item);
        }

        virtdir->busy = true;

        wchar_t *curpath = (wchar_t *)InterlockedCompareExchangePointer((volatile PVOID *)&virtdir->path,path,NULL);
        if(curpath != NULL)
        {
            free(path);
        }
    }
}

static void virtdir_filesystem_cancel_dirwalk(geek_virtdir_filesystem *virtdir)
{
    if(virtdir->find_handle != INVALID_HANDLE_VALUE)
    {
        FindClose(virtdir->find_handle);
        virtdir->find_handle = INVALID_HANDLE_VALUE;
        virtdir->busy = false;
    }
}

static void virtdir_filesystem_continue_dirwalk(geek_virtdir_filesystem *virtdir)
{
    WIN32_FIND_DATA find_data;
    BOOL res = FindNextFile(virtdir->find_handle,&find_data);
    if(res == FALSE)
    {
        FindClose(virtdir->find_handle);
        virtdir->find_handle = INVALID_HANDLE_VALUE;
        virtdir->busy = false;
        geek_virtdir_processor_on_end_dirwalk(virtdir->processor);
    }
    else
    {
        geek_diritem *item = geek_diritem_pool_alloc();
        virtdir_filesystem_fill_diritem(item,&find_data);
        geek_virtdir_processor_on_new_diritem(virtdir->processor,item);
    }
}

static unsigned __stdcall virtdir_filesystem_thread(void *virtdir_)
{
    geek_virtdir_filesystem *virtdir = (geek_virtdir_filesystem *)virtdir_;

    for(;;)
    {
        LONG msg = 0;

        if(virtdir->busy && !geek_msgmask_peek(&virtdir->msgmask,&msg))
        {
            return 0;
        }
        else if(!virtdir->busy && !geek_msgmask_recv(&virtdir->msgmask,&msg))
        {
            return 0;
        }

        if(msg != 0)
        {
            switch(msg)
            {
            case geek_vdfsm_begin_dirwalk:
                virtdir_filesystem_begin_dirwalk(virtdir);
                break;
            case geek_vdfsm_cancel_dirwalk:
                virtdir_filesystem_cancel_dirwalk(virtdir);
                break;
            case geek_vdfsm_quit:
            default:
                return 1;
            }
        }

        if(virtdir->find_handle != INVALID_HANDLE_VALUE)
        {
            virtdir_filesystem_continue_dirwalk(virtdir);
        }
    }
}

static void virtdir_filesystem_destroy(geek_virtdir *virtdir_)
{
    geek_virtdir_filesystem *virtdir = (geek_virtdir_filesystem *)virtdir_;

    if(virtdir->thread != NULL)
    {
        geek_msgmask_send(&virtdir->msgmask,geek_vdfsm_quit);
        WaitForSingleObject((HANDLE)virtdir->thread,5000);
        CloseHandle((HANDLE)virtdir->thread);
    }

    if(virtdir->path != NULL)
    {
        free((wchar_t *)virtdir->path);
    }

    geek_msgmask_destroy(&virtdir->msgmask);
    memset(virtdir,0,sizeof(geek_virtdir_filesystem));
}

static bool virtdir_filesystem_navigate(geek_virtdir *virtdir_, const wchar_t *path)
{
    geek_virtdir_filesystem *virtdir = (geek_virtdir_filesystem *)virtdir_;
    
    geek_msgmask_send(&virtdir->msgmask,geek_vdfsm_cancel_dirwalk);
    size_t pathlen = wcslen(path) + 5; // +4 for L'\\*.*'
                                       // +1 for L'\0'

    wchar_t *newpath = (wchar_t *)malloc(pathlen*sizeof(wchar_t));
    wcscpy_s(newpath,pathlen,path);
    wcscat_s(newpath,pathlen,L"\\*.*");

    wchar_t *oldpath = (wchar_t *)InterlockedExchangePointer(&virtdir->path,newpath);
    if(oldpath != NULL)
    {
        free(oldpath);
    }

    geek_msgmask_send(&virtdir->msgmask,geek_vdfsm_begin_dirwalk);
    return true;
}

bool geek_virtdir_filesystem_create(geek_virtdir_processor *p, geek_virtdir *virtdir_)
{
    geek_virtdir_filesystem *virtdir = (geek_virtdir_filesystem *)virtdir_;
    memset(virtdir,0,sizeof(geek_virtdir_filesystem));

    if(!geek_msgmask_create(&virtdir->msgmask))
    {
        goto error_exit;
    }

    virtdir->destroy_fn = &virtdir_filesystem_destroy;
    virtdir->navigate_fn = &virtdir_filesystem_navigate;

    virtdir->processor = p;
    virtdir->find_handle = INVALID_HANDLE_VALUE;

    virtdir->thread = _beginthreadex(NULL,0,&virtdir_filesystem_thread,virtdir,0,NULL);
    if(virtdir->thread == 0)
    {
        goto error_exit;
    }

    return true;

error_exit:
    virtdir_filesystem_destroy(virtdir);
    return false;
}
